#ifndef LIBSIM_MESSAGE_QUEUE_H_INCLUDED
#define LIBSIM_MESSAGE_QUEUE_H_INCLUDED

#include "logpp/configs.h"
#include "logpp/utils/memory.h"
#include "logpp/utils/math.h"
#include <condition_variable>
#include <chrono>
#include <mutex>

LOGPP_NS_BEGIN

template <typename T>
class message_queue {
public:
    using value_type = T;
    using pointer = T*;
    using reference = T&;
    using const_pointer = const T*;
    using const_reference = const T&;

    static_assert(std::is_nothrow_default_constructible_v<T>,
        "T must be default constructible");

    struct configuration {
        size_t max_size;
        size_t pause_size;
    };

    ~message_queue()
    {
        release();
    }

    message_queue() noexcept = default;

    int initialize(const configuration& config) noexcept
    {
        return init_buffer(config);
    }

    auto max_size() const noexcept
    {
        return initialized() ? data_->max_size : 0;
    }

    auto capacity() const noexcept
    {
        return initialized() ? data_->capacity : 0;
    }

    auto size() const noexcept
    {
        return initialized() ? (data_->head - data_->tail) : 0;
    }

    auto empty() const noexcept
    {
        return size() == 0;
    }

    auto full() const noexcept
    {
        return size() == max_size();
    }

    void swap(message_queue &other) noexcept
    {
        std::swap(data_, other.data_);
    }

    bool initialized() const noexcept
    {
        return data_ != nullptr;
    }

    message_queue(const message_queue& other) = delete;
    message_queue& operator=(const message_queue& other) = delete;

    message_queue(message_queue&& other) noexcept
    {
        swap(other);
    }

    message_queue& operator=(message_queue&& other) noexcept
    {
        if (&other != this) {
            swap(other);
        }
    }

    bool try_push(const_reference v)
        noexcept(std::is_nothrow_copy_assignable_v<T>)
    {
        if (flow_control()) {
            return false;
        }

        auto index = next_head();
        if (index == npos) return false;
        data_->values[mod(index)] = v;
        data_->read_cv.notify_one();
        return true;
    }

    bool try_pop(reference v)
        noexcept(std::is_nothrow_copy_assignable_v<T>)
    {
        auto index = next_tail();
        if (index == npos) return false;
        v = data_->values[mod(index)];
        data_->write_cv.notify_one();
        return true;
    }

    bool push(const_reference v, std::chrono::milliseconds timeout)
        noexcept(std::is_nothrow_copy_assignable_v<T>)
    {
        if (!initialized()) {
            return false;
        }
        return push_timeout(v, timeout);
    }

    bool pop(reference v, std::chrono::milliseconds timeout)
        noexcept(std::is_nothrow_copy_assignable_v<T>)
    {
        if (!initialized()) {
            return false;
        }
        return pop_timeout(v, timeout);
    }

private:
    static constexpr size_t npos = -1U;

    size_t mod(size_t index) const noexcept
    {
        return index & (data_->capacity - 1);
    }

    size_t next_head() noexcept
    {
        if (full()) {
            return npos;
        }
        return next(data_->head);
    }

    size_t next_tail() noexcept
    {
        if (empty()) {
            return npos;
        }
        return next(data_->tail);
    }

    size_t next(size_t &curr) noexcept
    {
        auto index = curr++;
        return index;
    }

    int init_buffer(const configuration& cfg) noexcept
    {
        auto capacty = math::clp2(cfg.max_size);
        if (capacty == 0 || capacty < cfg.max_size) {
            return -1;
        }

        auto alloc = logpp::allocator<std::byte>();
        auto mptr = alloc.allocate(sizeof(buffer_data) + sizeof(T) * capacty);
        if (mptr == nullptr) {
            return -2;
        }

        auto tmp = (buffer_data*)mptr;
        auto alloc1 = logpp::allocator<T>();
        for (auto i = 0; i < capacty; ++i) {
            alloc1.construct(&tmp->values[i]);
        }
    
        tmp->capacity = capacty;
        tmp->max_size = cfg.max_size;
        tmp->pause_size = cfg.pause_size;
        tmp->head = 0;
        tmp->tail = 0;
        data_ = tmp;
        return 0;
    }

    void release()
    {
        if (data_ != nullptr) {
            auto bytes = sizeof(buffer_data) + sizeof(T) * data_->capacity;
            auto alloc = logpp::allocator<T>();
            for (auto i = 0; i < data_->capacity; ++i) {
                alloc.destroy(&data_->values[i]);
            }
            logpp::allocator<std::byte>().deallocate((std::byte*)data_, bytes);
            data_ = nullptr;
        }
    }

    bool push_timeout(const_reference v, std::chrono::milliseconds timeout)
        noexcept(std::is_nothrow_copy_assignable_v<T>)
    {
        if (timeout.count() == 0) {
            return try_push(v);
        }

        std::unique_lock<std::mutex> lock{data_->write_mtx};
        data_->write_cv.wait_for(lock, timeout, [&]() {
            return !flow_control();
        });

        return try_push(v);
    }

    bool pop_timeout(reference v, std::chrono::milliseconds timeout)
        noexcept(std::is_nothrow_copy_assignable_v<T>)
    {
        if (timeout.count() == 0) {
            return try_pop(v);
        }

        std::unique_lock<std::mutex> lock{data_->read_mtx};
        data_->read_cv.wait_for(lock, timeout, [&]() {
            return !empty();
        });

        return try_pop(v);
    }

    bool flow_control() const noexcept
    {
        return size() >= data_->pause_size;
    }

    struct buffer_data {
        std::mutex read_mtx;
        std::mutex write_mtx;
        std::condition_variable read_cv;
        std::condition_variable write_cv;

        size_t capacity{0};
        size_t max_size{0};
        size_t pause_size{0};
        size_t head{0};
        size_t tail{0};
        value_type values[];
    };

    buffer_data *data_{nullptr};
};

LOGPP_NS_END

#endif /* LIBSIM_MESSAGE_QUEUE_H_INCLUDED */
